/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.console.config.bean;

import org.jline.reader.Candidate;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
import org.openislands.oi.console.config.LineReaderConfig;
import org.openislands.oi.console.config.annotation.Command;
import org.openislands.oi.console.config.annotation.CommandArgs;
import org.openislands.oi.console.config.annotation.CommandGroup;
import org.openislands.oi.console.config.annotation.CommandParam;
import org.openislands.oi.console.config.completer.ParamsCompleter;
import org.openislands.oi.constant.Dict;
import org.openislands.oi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

@Component
public class CommandGroupContext implements LoadBeanHandler {
    private static final Logger log = LoggerFactory.getLogger(CommandGroupContext.class);

    /***
     * Command group all command method cache
     * Load see {@link #initBean(Map)}
     */
    private static final Map<String, Map<String, Method>> commandGroupCache = new HashMap<>();

    @Override
    public void loadBean(Set<Class<?>> classes, Map<Class<?>, List<Object>> resultMap) {
    }

    /***
     * Init all annotation present {@link CommandGroup} class
     * Load all command under command group to {@link #commandGroupCache}
     * Load group, command, parameter Completer. see {@link #initChildCommand(Class, Map, String, List, Map)}
     * @param beanMap bean container
     */
    @Override
    public void initBean(Map<Class<?>, List<Object>> beanMap) {
        for (Class<?> aClass : beanMap.keySet()) {
            try {
                CommandGroup commandGroup = aClass.getAnnotation(CommandGroup.class);
                if (Objects.nonNull(commandGroup)) {
                    String value = commandGroup.value();
                    if (!StringUtils.noTrimEmpty(value)) {
                        value = aClass.getSimpleName();
                    }

                    Map<String, Method> childCommand = commandGroupCache.get(value);
                    if (Objects.isNull(childCommand)) {
                        childCommand = new HashMap<>();
                    }

                    // load all child command and command candidates
                    List<Candidate> commandList = new ArrayList<>();
                    Map<String, List<Candidate>> paramCandidates = new HashMap<>();
                    initChildCommand(aClass, childCommand, value, commandList, paramCandidates);
                    LineReaderConfig.addCompleter(new ArgumentCompleter(
                            new StringsCompleter(new Candidate(value, value, commandGroup.service(),
                                    null, null, null, true)),
                            new StringsCompleter(commandList.toArray(new Candidate[0])),
                            new ParamsCompleter(paramCandidates)
                    ));
                    commandGroupCache.put(value, childCommand);
                    log.debug("load command group {} done", value);
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    /***
     * Load command, parameter Completer candidates
     * @param aClass            declaring class
     * @param childCommand      class command,method map
     * @param groupName         command group name
     * @param commandList       group all command candidates
     * @param paramCandidates   command,parameters candidates
     */
    private void initChildCommand(Class<?> aClass, Map<String, Method> childCommand, String groupName,
                                  List<Candidate> commandList, Map<String, List<Candidate>> paramCandidates) {
        Method[] declaredMethods = aClass.getDeclaredMethods();

        String n = Dict.aNull();
        for (Method declaredMethod : declaredMethods) {
            Command command = declaredMethod.getAnnotation(Command.class);
            if (Objects.nonNull(command)) {
                String name = command.name();
                if (!StringUtils.noTrimEmpty(name)) {
                    name = command.value();
                }
                if (!StringUtils.noTrimEmpty(name)) {
                    name = declaredMethod.getName();
                }
                // add method
                childCommand.put(name, declaredMethod);

                String paramNameKey = ParamsCompleter.getNameKey(groupName, name);
                List<Candidate> paramCandidate = new ArrayList<>();
                Parameter[] parameters = declaredMethod.getParameters();
                for (Parameter parameter : parameters) {
                    // is the command parameter list
                    if (parameter.isAnnotationPresent(CommandArgs.class)) {
                        continue;
                    }
                    CommandParam commandParam = parameter.getAnnotation(CommandParam.class);
                    String paramName;
                    String desc = null;
                    if (Objects.nonNull(commandParam)) {
                        paramName = LineReaderConfig.defaultParamName(parameter.getName(), commandParam);
                        desc = commandParam.desc().isEmpty() ? null : commandParam.desc();
                    } else {
                        paramName = LineReaderConfig.defaultParamName(parameter.getName());
                    }
                    paramCandidate.add(new Candidate(paramName, paramName, paramNameKey, desc, n, n, true));
                }

                // add method command
                commandList.add(new Candidate(name, name, groupName, n, n, n, true));
                // add command params
                paramCandidates.put(paramNameKey, paramCandidate);
                log.debug("load command group {} child command {}", groupName, name);
            }
        }
    }

    public static Set<Map.Entry<String, Map<String, Method>>> entrySet() {
        return commandGroupCache.entrySet();
    }

    public Map<String, Method> getChildCommand(String groupName) {
        return commandGroupCache.get(groupName);
    }
}